// License: GPL. Copyright 2007 by Immanuel Scholz and others
package org.openstreetmap.josm.gui.tagging.ac;

import java.awt.Component;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.Collection;

import javax.swing.ComboBoxEditor;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.ListCellRenderer;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import javax.swing.text.PlainDocument;
import javax.swing.text.StyleConstants;

/**
 * @author guilhem.bonnefille@gmail.com
 */
@SuppressWarnings("serial")
public class AutoCompletingComboBox extends JComboBox {

    private boolean autocompleteEnabled = true;

    /**
     * Auto-complete a JComboBox.
     * 
     * Inspired by http://www.orbital-computer.de/JComboBox/
     */
    class AutoCompletingComboBoxDocument extends PlainDocument {
        private JComboBox comboBox;
        private boolean selecting = false;

        public AutoCompletingComboBoxDocument(final JComboBox comboBox) {
            this.comboBox = comboBox;
        }

        @Override
        public void remove(int offs, int len) throws BadLocationException {
            if (selecting)
                return;
            super.remove(offs, len);
        }

        @Override
        public void insertString(int offs, String str, AttributeSet a)
                throws BadLocationException {
            if (selecting || (offs == 0 && str.equals(getText(0, getLength()))))
                return;
            boolean initial = (offs == 0 && getLength() == 0 && str.length() > 1);
            super.insertString(offs, str, a);

            // return immediately when selecting an item
            // Note: this is done after calling super method because we need
            // ActionListener informed
            if (selecting)
                return;
            if (!autocompleteEnabled)
                return;
            // input method for non-latin characters (e.g. scim)
            if (a != null && a.isDefined(StyleConstants.ComposedTextAttribute))
                return;

            int size = getLength();
            int start = offs + str.length();
            int end = start;
            String curText = getText(0, size);

            // if the text starts with a number we don't autocomplete
            // TODO deal with numbers
            /*
             * if (Main.pref.getBoolean("autocomplete.dont_complete_numbers",
             * true)) { try { Long.parseLong(str); if (curText.length() == 0) //
             * we don't autocomplete on numbers return; Long.parseLong(curText);
             * return; } catch (NumberFormatException e) { // either the new
             * text or the current text isn't a number. We continue with //
             * autocompletion } }
             */
            // lookup and select a matching item
            Object item = lookupItem(curText);
            setSelectedItem(item);
            if (initial) {
                start = 0;
            }
            if (item != null) {
                String newText = ((AutoCompletionListItem) item).getValue();
                if (!newText.equals(curText)) {
                    selecting = true;
                    super.remove(0, size);
                    super.insertString(0, newText, a);
                    selecting = false;
                    start = size;
                    end = getLength();
                }
            }
            JTextComponent editor = (JTextComponent) comboBox.getEditor()
                    .getEditorComponent();
            editor.setSelectionStart(start);
            editor.setSelectionEnd(end);
        }

        private void setSelectedItem(Object item) {
            selecting = true;
            comboBox.setSelectedItem(item);
            selecting = false;
        }

        private Object lookupItem(String pattern) {
            ComboBoxModel model = comboBox.getModel();
            AutoCompletionListItem bestItem = null;
            for (int i = 0, n = model.getSize(); i < n; i++) {
                AutoCompletionListItem currentItem = (AutoCompletionListItem) model
                        .getElementAt(i);
                if (currentItem.getValue().equals(pattern)) {
                    return currentItem;
                }
                if (currentItem.getValue().startsWith(pattern)) {
                    if (bestItem == null
                            || currentItem.getPriority().compareTo(
                                    bestItem.getPriority()) > 0) {
                        bestItem = currentItem;
                    }
                }
            }
            return bestItem; // may be null
        }
    }

    /**
     * Creates a new combo box with auto completion support.
     */
    public AutoCompletingComboBox() {
        setRenderer(new AutoCompleteListCellRenderer());
        final JTextComponent editor = (JTextComponent) this.getEditor()
                .getEditorComponent();
        editor.setDocument(new AutoCompletingComboBoxDocument(this));
        editor.addFocusListener(new FocusListener() {
            public void focusLost(FocusEvent e) {
            }

            public void focusGained(FocusEvent e) {
                editor.selectAll();
            }
        });
    }

    /**
     * Convert the selected item into a String that can be edited in the editor
     * component.
     * 
     * @param editor
     *            the editor
     * @param item
     *            accepts AutoCompletionListItem, String and null
     */
    @Override
    public void configureEditor(ComboBoxEditor editor, Object item) {
        if (item == null) {
            editor.setItem(null);
        } else if (item instanceof String) {
            editor.setItem(item);
        } else if (item instanceof AutoCompletionListItem) {
            editor.setItem(((AutoCompletionListItem) item).getValue());
        } else
            throw new IllegalArgumentException();
    }

    /**
     * Selects a given item in the ComboBox model
     * 
     * @param item
     *            accepts AutoCompletionListItem, String and null
     */
    @Override
    public void setSelectedItem(Object item) {
        if (item == null) {
            super.setSelectedItem(null);
        } else if (item instanceof AutoCompletionListItem) {
            super.setSelectedItem(item);
        } else if (item instanceof String) {
            String s = (String) item;
            // find the string in the model or create a new item
            for (int i = 0; i < getModel().getSize(); i++) {
                AutoCompletionListItem acItem = (AutoCompletionListItem) getModel()
                        .getElementAt(i);
                if (s.equals(acItem.getValue())) {
                    super.setSelectedItem(acItem);
                    return;
                }
            }
            super.setSelectedItem(new AutoCompletionListItem(s,
                    AutoCompletionItemPritority.UNKNOWN));
        } else
            throw new IllegalArgumentException();
    }

    /**
     * sets the items of the combobox to the given strings
     */
    public void setPossibleItems(Collection<String> elems) {
        DefaultComboBoxModel model = (DefaultComboBoxModel) this.getModel();
        Object oldValue = this.getEditor().getItem();
        model.removeAllElements();
        for (String elem : elems) {
            model.addElement(new AutoCompletionListItem(elem,
                    AutoCompletionItemPritority.UNKNOWN));
        }
        this.getEditor().setItem(oldValue);
    }

    /**
     * sets the items of the combobox to the given AutoCompletionListItems
     */
    public void setPossibleACItems(Collection<AutoCompletionListItem> elems) {
        DefaultComboBoxModel model = (DefaultComboBoxModel) this.getModel();
        Object oldValue = this.getEditor().getItem();
        model.removeAllElements();
        for (AutoCompletionListItem elem : elems) {
            model.addElement(elem);
        }
        this.getEditor().setItem(oldValue);
    }

    protected boolean isAutocompleteEnabled() {
        return autocompleteEnabled;
    }

    protected void setAutocompleteEnabled(boolean autocompleteEnabled) {
        this.autocompleteEnabled = autocompleteEnabled;
    }

    /**
     * ListCellRenderer for AutoCompletingComboBox renders an
     * AutoCompletionListItem by showing only the string value part
     */
    public static class AutoCompleteListCellRenderer extends JLabel implements
            ListCellRenderer {

        /**
         * Creates a new instance of the cell renderer.
         */
        public AutoCompleteListCellRenderer() {
            setOpaque(true);
        }

        public Component getListCellRendererComponent(JList list, Object value,
                int index, boolean isSelected, boolean cellHasFocus) {
            if (isSelected) {
                setBackground(list.getSelectionBackground());
                setForeground(list.getSelectionForeground());
            } else {
                setBackground(list.getBackground());
                setForeground(list.getForeground());
            }

            AutoCompletionListItem item = (AutoCompletionListItem) value;
            setText(item.getValue());
            return this;
        }
    }
}
